/* * Copyright (c) 2014 tabletoptool.com team. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * rptools.com team - initial implementation * tabletoptool.com team - further development */ package com.t3.swing; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Paint; import java.awt.Rectangle; import java.awt.datatransfer.Transferable; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DragSourceEvent; import java.awt.dnd.DragSourceListener; import java.awt.dnd.DragSourceMotionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.swing.JComponent; import javax.swing.JScrollPane; import javax.swing.Scrollable; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; public class ImagePanel extends JComponent implements Scrollable, DragGestureListener, DragSourceListener, MouseListener, DragSourceMotionListener { public enum SelectionMode { SINGLE, MULTIPLE, NONE }; private ImagePanelModel model; private int gridSize = 50; private final int gridPadding = 5; private final Map<Rectangle, Integer> imageBoundsMap = new HashMap<Rectangle, Integer>(); private boolean isDraggingEnabled = true; private boolean showCaptions = true; private boolean showImageBorder = false; private SelectionMode selectionMode = SelectionMode.NONE; private final List<Object> selectedIDList = new ArrayList<Object>(); private final List<SelectionListener> selectionListenerList = new ArrayList<SelectionListener>(); private int fontHeight; public ImagePanel() { DragSource.getDefaultDragSource().createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_COPY_OR_MOVE, this); addMouseListener(this); // Register with the ToolTipManager so that tooltips from the // renderer show through. ToolTipManager toolTipManager = ToolTipManager.sharedInstance(); toolTipManager.registerComponent(this); } public void setGridSize(int size) { // Min if (size < 25) { size = 25; } gridSize = size; revalidate(); repaint(); } public void setDraggingEnabled(boolean enabled) { isDraggingEnabled = enabled; } public void setSelectionMode(SelectionMode mode) { selectionMode = mode; selectedIDList.clear(); repaint(); } public void setShowCaptions(boolean enabled) { showCaptions = enabled; repaint(); } public void setShowImageBorders(boolean enabled) { showImageBorder = enabled; repaint(); } public ImagePanelModel getModel() { return model; } public void setModel(ImagePanelModel model) { this.model = model; revalidate(); JScrollPane scrollPane = (JScrollPane) SwingUtilities.getAncestorOfClass(JScrollPane.class, this); if (scrollPane != null) { scrollPane.revalidate(); scrollPane.repaint(); } } public List<Object> getSelectedIds() { List<Object> list = new ArrayList<Object>(); list.addAll(selectedIDList); return list; } public void clearSelection() { selectedIDList.clear(); fireSelectionEvent(); } public void addSelectionListener(SelectionListener listener) { selectionListenerList.add(listener); } public void removeSelectionListener(SelectionListener listener) { selectionListenerList.remove(listener); } @Override public boolean isOpaque() { return true; } @Override protected void paintComponent(Graphics g) { Rectangle clipBounds = g.getClipBounds(); Dimension size = getSize(); FontMetrics fm = g.getFontMetrics(); fontHeight = fm.getHeight(); g.setColor(getBackground()); g.fillRect(0, 0, size.width, size.height); if (model == null) { return; } imageBoundsMap.clear(); int x = gridPadding; int y = gridPadding; for (int i = 0; i < model.getImageCount(); i++) { Image image = model.getImage(i); Rectangle bounds = new Rectangle(x, y, gridSize, gridSize); imageBoundsMap.put(bounds, i); // Background Paint paint = model.getBackground(i); if (paint != null) { ((Graphics2D)g).setPaint(paint); g.fillRect(x-2, y-2, gridSize+4, gridSize+4); //bleed out a little } if (image != null && bounds.intersects(clipBounds)) { Dimension dim = constrainSize(image, gridSize); g.drawImage(image, x + (gridSize - dim.width)/2, y + (gridSize - dim.height)/2, dim.width, dim.height, this); // Image border if (showImageBorder) { g.setColor(Color.black); g.drawRect(bounds.x, bounds.y, bounds.width, bounds.height); } } // Selected if (selectedIDList.contains(model.getID(i))) { // TODO: Let the user pick the border ImageBorder.RED.paintAround((Graphics2D) g, bounds.x, bounds.y, bounds.width, bounds.height); } // Decorations Image[] decorations = model.getDecorations(i); if (decorations != null) { int offx = x; int offy = y + gridSize; int rowHeight = 0; for (Image decoration : decorations) { g.drawImage(decoration, offx, offy - decoration.getHeight(null), this); rowHeight = Math.max(rowHeight, decoration.getHeight(null)); offx += decoration.getWidth(null); if (offx > gridSize) { offx = x; offy -= rowHeight + 2; rowHeight = 0; } } } // Caption if (showCaptions) { String caption = model.getCaption(i); if (caption != null) { boolean nameTooLong = false; int strWidth = fm.stringWidth(caption); while (strWidth > bounds.width) { nameTooLong = true; caption = caption.substring(0, caption.length()-1); strWidth = fm.stringWidth(caption + "..."); } if (nameTooLong) { caption += "..."; } int cx = x + (gridSize - strWidth) / 2; int cy = y + gridSize + fm.getHeight(); g.setColor(getForeground()); g.drawString(caption, cx, cy); } } // Line wrap x += gridSize + gridPadding; if (x > size.width - gridPadding - gridSize) { x = gridPadding; y += gridSize + gridPadding; if (showCaptions) { y += fontHeight; } } } } protected int getIndex(int x, int y) { for (Entry<Rectangle, Integer> entry : imageBoundsMap.entrySet()) { if (entry.getKey().contains(x, y)) { return entry.getValue(); } } return -1; } protected Object getImageIDAt(int x, int y) { return model != null ? model.getID(getIndex(x, y)) : null; } protected void fireSelectionEvent() { List<Object> selectionList = Collections.unmodifiableList(selectedIDList); for (int i = 0; i < selectionListenerList.size(); i++) { selectionListenerList.get(i).selectionPerformed(selectionList); } } private Dimension constrainSize(Image image, int size) { int imageWidth = image.getWidth(this); int imageHeight = image.getHeight(this); if (imageWidth == imageHeight) { return new Dimension(size, size); } int width = 0; int height = 0; if (imageWidth > imageHeight) { width = size; height = (int)(imageHeight * ((float)size) / imageWidth); } else { height = size; width = (int)(imageWidth * ((float)size) / imageHeight); } return new Dimension(width, height); } @Override public Dimension getPreferredSize() { int width = getSize().width; int rowCount = 0; if (width < gridSize + gridPadding*2) { rowCount = model != null ? model.getImageCount() : 0; } else { rowCount = (int)(model != null ? Math.ceil(model.getImageCount() / Math.floor(width / (gridSize + gridPadding))) : 0); } int height = (model != null ? rowCount * (gridSize + gridPadding + fontHeight) + gridPadding : gridSize + gridPadding); return new Dimension(width, height); } @Override public Dimension getMinimumSize() { return getPreferredSize(); } protected int getImageIndexAt(int x, int y) { for (Rectangle rect : imageBoundsMap.keySet()) { if (rect.contains(x, y)) { return imageBoundsMap.get(rect); } } return -1; } @Override public String getToolTipText(MouseEvent event) { if (getModel() == null) { return null; } return getModel().getCaption(getIndex(event.getX(), event.getY())); } // SCROLLABLE @Override public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } @Override public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { return (gridSize + gridPadding) * 2; } @Override public boolean getScrollableTracksViewportHeight() { Dimension parentSize = SwingUtilities.getAncestorOfClass(JScrollPane.class, this).getSize(); return getPreferredSize().height < parentSize.height; } @Override public boolean getScrollableTracksViewportWidth() { return true; } @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { return gridSize / 4; } // DRAG GESTURE LISTENER @Override public void dragGestureRecognized(DragGestureEvent dge) { if (model == null || !isDraggingEnabled) { return; } int index = getImageIndexAt(dge.getDragOrigin().x, dge.getDragOrigin().y); if (index < 0) { return; } Transferable transferable = model.getTransferable(index); if (transferable == null) { return; } //dge.startDrag(Toolkit.getDefaultToolkit().createCustomCursor(model.getImage(index), new Point(0, 0), "Thumbnail"), transferable, this); dge.startDrag(getDragCursor(), transferable, this); DragSource.getDefaultDragSource().addDragSourceMotionListener(this); } protected Cursor getDragCursor() { return null; } // DRAG SOURCE LISTENER @Override public void dragDropEnd(DragSourceDropEvent dsde) { DragSource.getDefaultDragSource().removeDragSourceMotionListener(this); } @Override public void dragEnter(DragSourceDragEvent dsde) {} @Override public void dragExit(DragSourceEvent dse) {} @Override public void dragOver(DragSourceDragEvent dsde) {} @Override public void dropActionChanged(DragSourceDragEvent dsde) {} // DRAG SOURCE MOTION LISTENER @Override public void dragMouseMoved(DragSourceDragEvent dsde) {} // MOUSE LISTENER @Override public void mouseClicked(MouseEvent e) {} @Override public void mouseEntered(MouseEvent e) {} @Override public void mouseExited(MouseEvent e) {} @Override public void mousePressed(MouseEvent e) { if (selectionMode == SelectionMode.NONE) { return; } Object imageID = getImageIDAt(e.getX(), e.getY()); // TODO: Handle shift too if (!SwingUtil.isControlDown(e) || selectionMode == SelectionMode.SINGLE) { selectedIDList.clear(); } if (imageID != null) { selectedIDList.add(imageID); repaint(); } fireSelectionEvent(); } @Override public void mouseReleased(MouseEvent e) {} }